#include <stdlib.h>
#include <string>
#include <string.h>
#include <vector>
#include <sstream>
#include <iostream>
#include <errno.h>
#include "PopupServer.hpp"
#include "PopupOSAL.hpp"
#include "msg/PopupMessages.hpp"

using namespace std;
using namespace Popup;
using namespace PopupUtils;

//=============================================================================
// Implementation of PopupServer static instantiation method
//=============================================================================

Server *Server::newServer(unsigned short p_port,
                          unsigned short p_maxNbConnections,
                          DatabaseUI *p_database,
                          ServerUI *p_serverUI)
{
  return ServerImpl::createInstance(p_port, p_maxNbConnections,
      p_database, p_serverUI);
}

//=============================================================================
// Construction/Destruction
//=============================================================================

ServerImpl *ServerImpl::instance = 0;

ServerImpl::ServerImpl(unsigned short p_port,
                          unsigned short p_maxNbConnections,
                          DatabaseUI *p_database,
                          ServerUI *p_serverUI)
: database(p_database), ui(p_serverUI), exit(false), rsahelper()
{
  string _errMsg;
  serverSocket = PopupUtils::Socket::newServerSocket(p_port,
      p_maxNbConnections, &_errMsg);
  if (serverSocket == 0) {
    error(_errMsg.c_str());
  }

  // Get all users from database
  if (!p_database->getAllUsers(loginsMap)) {
    error("Ooops! Failed to retrieve users list from database");
  }
}
//---------------------------------------------------------

ServerImpl::~ServerImpl()
{
  instance = 0;
}

//---------------------------------------------------------

ServerImpl *ServerImpl::createInstance(unsigned short p_port,
                                       unsigned short p_maxNbConnections,
                                       DatabaseUI *p_database,
                                       ServerUI *p_serverUI)
{
  if (instance != 0) {
    error("Cannot create a new server. An existing one is running");
    return 0;
  }
  else
  {
    instance = new ServerImpl(p_port, p_maxNbConnections,
        p_database, p_serverUI);
    if (instance->serverSocket != 0) {
#ifndef WIN32
      ::signal(SIGTERM, ServerImpl::signalHandler);
      ::signal(SIGINT,  ServerImpl::signalHandler);
      ::signal(SIGSTOP, ServerImpl::signalHandler);
      ::signal(SIGPIPE, SIG_IGN);
#endif
      return instance;
    } else {
      return 0;
    }
  }
}
//---------------------------------------------------------

ServerImpl *ServerImpl::getInstance()
{
  if (instance == 0) {
    error("No instance of the server is currently running!");
    return 0;
  }
  else {
    return instance;
  }
}
//---------------------------------------------------------

void ServerImpl::signalHandler(int p_sig)
{
  trace("Received signal %d\n", p_sig);
  if (instance != 0) {
    instance->kill();
    instance->closeAllConnections();
  }
  ::exit(EXIT_FAILURE);
}
//---------------------------------------------------------

void ServerImpl::kill()
{
  this->exit = true;
}

//=============================================================================
// Connections management
//=============================================================================

void ServerImpl::run()
{
  fd_set _readfds;
  fd_set _errfds;
  Sockets::iterator _itsock;
  Sessions::iterator _itsession;
  struct timeval _tv;
  int _selectRc = 0;
  int _nfds = 0;

  // This function is dedicated to listen function
  info("Entering main loop...");

  // Indefinitely read...
  while (!exit) {

    // Setup sockets file descriptors set
    FD_ZERO(&_readfds);
    FD_ZERO(&_errfds);
    // Add accept socket
    _nfds = serverSocket->fd();
    FD_SET(_nfds, &_readfds);

    _itsock = sockets.begin();
    while (_itsock != sockets.end()) {
      if (_itsock->second->disconnect) {
        deleteSession(_itsock->second);
        sockets.erase(_itsock++);
      } else {
        FD_SET(_itsock->first, &_readfds);
        FD_SET(_itsock->first, &_errfds);
        // We need to save the highest socket value we want to poll
        if (_itsock->first > _nfds) _nfds = _itsock->first;
        ++_itsock;
      }
    }

    _tv.tv_sec  = 0;
    _tv.tv_usec = 5000;

    _selectRc = select(_nfds + 1, &_readfds, 0, &_errfds, &_tv);

    // Error
    if (_selectRc == -1)
    {
      error("select() : %s", strerror(errno));
    }
    // Success
    else if (_selectRc > 0)
    {
      // Handle activity for each selected session

      for (_itsock = sockets.begin(); _itsock != sockets.end(); _itsock++)
      {
        // Process data on the selected socket
        if (FD_ISSET(_itsock->first,  &_readfds)) {
          if (!processActivity(_itsock->second)) {
            // Close session/socket in case we failed to manage activity
            _itsock->second->disconnect = true;
          }
        }
        // Process errors: close session/socket!
        else if (FD_ISSET(_itsock->first, &_errfds)) {
            _itsock->second->disconnect = true;
        }
      }

      // New clients to accept?
      if (FD_ISSET(serverSocket->fd(), &_readfds)) {
        acceptNewClient();
      }
    }
    // Timeout
    else {}
  }

  info("Exiting main loop...");
  closeAllConnections();
}

void ServerImpl::acceptNewClient()
{
  string _errMsg;
  PopupUtils::Socket *_s = serverSocket->accept(&_errMsg);
  if (_s != 0)
  {
    trace("New connection!");
    _s->enableKeepAlive(true, 10);
    ClientSession *_session = new ClientSession(_s, &rsahelper);
    sockets[_s->fd()] = _session;
  }
  else
  {
    error(_errMsg.c_str());
  }
}

bool ServerImpl::processActivity(ClientSession *p_session)
{
  bool _closeConnection = false;
  Message *_message = 0;
  RawMessage *_fullMessage = 0;
  unsigned int _fullMessageSize = 0;

  if (p_session->networker.receive(&_message, &_fullMessage, &_fullMessageSize))
  {
    switch (_message->header.type)
    {
      case POPUP_MSG_TYPE_RSA_PUBLIC_KEY: {
        trace("Received client RSA public key");
        RsaPublicKeyMsg remotekey(_message);
        if (remotekey.isValid()) {
          p_session->sslContext.setRemoteRSApublicKey(remotekey.getKey(),
                                                    remotekey.getSize());
          trace("Sending my RSA public key");
          RsaPublicKeyMsg localkey(&p_session->sslContext);
          p_session->networker.send(&localkey);
        } else {
          error(
                            "Invalid RSA key message. Client rejected!");
          _closeConnection = true;
        }
        break;
      }
      case POPUP_MSG_TYPE_BLOWFISH_KEY: {
        trace("Received blowfish key");
        BlowfishKeyMsg blowfishKey(_message);
        if (blowfishKey.isValid()) {
          p_session->sslContext.setRemoteBlowfishKey(blowfishKey.getKey());
          trace("Handshake succeeded!");
        }
        break;
      }
      case POPUP_MSG_TYPE_LOGIN: {
        trace("Received login message");
        LoginMsg loginMsg(_message);
        ConnectionEcode _dbrc = POPUP_CONNECTION_ERR_UNKNOWN;
        string errorMsg = "Success!";
        if (loginMsg.isValid()) {

          // Search for corresponding user in our map
          UserLoginMap::iterator _it = loginsMap.find(loginMsg.getLogin());

          // Login creation request ---------------------------

          if (loginMsg.isCreateRequest())
          {
            // Too bad! It already exists!
            if (_it != loginsMap.end())
            {
              _dbrc = POPUP_CONNECTION_ERR_LOGIN_ALREADY_EXISTS;
            }
            // OK to create!
            else
            {
              trace("Creating new client: %s", loginMsg.getLogin().c_str());
              _dbrc = database->newUser(loginMsg.getLogin(),
                                        loginMsg.getPassword(),
                                        loginMsg.getLogin(),
                                        ui->getDefaultAvatar(),
                                        &p_session->user,
                                        errorMsg);

              if (_dbrc == POPUP_CONNECTION_SUCCESS) {
                // Add user into all users list
                loginsMap[loginMsg.getLogin()] = p_session->user;
              } else {
                error("Creation failed");
              }
            }
          }

          // Standard connection try ------------------------
          else
          {
            // User not found!
            if (_it == loginsMap.end())
            {
              _dbrc = POPUP_CONNECTION_ERR_UNKNOWN_USER;
            }
            // User found! Validate password!
            else if (database->authenticate(loginMsg.getLogin(),
                                            loginMsg.getPassword(),
                                            errorMsg))
            {
              trace("Authentication success");
              _dbrc = POPUP_CONNECTION_SUCCESS;
              p_session->user = _it->second;
            }
            else
            {
              _dbrc = POPUP_CONNECTION_ERR_INVALID_PASSWORD;
              trace("Authentication failed");
            }
          }
        }
        if (_dbrc != POPUP_CONNECTION_SUCCESS) {

          trace("Sending NACK to client %s", loginMsg.getLogin().c_str());
          ConnectionAck _ack(_dbrc, errorMsg);
          p_session->networker.send(&_ack);
        }
        else
        {
          bool alreadyConnected = false;

          // Make sure user is not already connected
          alreadyConnected = (sessions.find(p_session->user->getID()) !=
              sessions.end());

          if (alreadyConnected)
          {
            error("Client %s already connected!",
                              loginMsg.getLogin().c_str());
            ConnectionAck _ack(POPUP_CONNECTION_ERR_USER_ALREADY_CONNECTED,
                               "Strange... You are already connected!!");
            p_session->networker.send(&_ack);
          }

          // OK!!
          else
          {
            User *_user = p_session->user;

            _user->setAlive(true);

            // Allocate a new session number for this user
            short _rand;
            PopupUtils::randomString((char*)&_rand, sizeof(unsigned short));
            p_session->id = (_rand << 16) | _user->getID();

            info("Client %s connected.", loginMsg.getLogin().c_str());
            ConnectionAck _ack(p_session->id, *_user);
            p_session->networker.send(&_ack);
            ui->onClientConnected(*_user);

            // Notify all users about this new client connection
            UserUpdateMsg _newUserUpdateMessage(*_user);
            broadcastMessage(&_newUserUpdateMessage);

            // Send user statistics
            string _errMsg;
            StatisticsUpdateMsg _statsUpdateMessage(_user->getID());
            if (database->getUserStatitics(_user->getID(),
                                           _statsUpdateMessage.statistics(),
                                           _errMsg)) {
              broadcastMessage(&_statsUpdateMessage);
            }

            // Ok! Register session
            sessions[_user->getID()] = p_session;
            p_session->registered = true;

            // Send list of all user to this new client
            sendAllUsersList(p_session);
          }
        }
        break;
      }
      case POPUP_MSG_TYPE_MESSAGE: {
        UserMessageWrapper msg(_message);
        sendMessage(_fullMessage, _fullMessageSize, msg.targets);
        break;
      }
      case POPUP_MSG_TYPE_ATTACHMENT: {
        AttachmentMsg msg(_message);
        sendMessage(_fullMessage, _fullMessageSize, msg.getTargets());
        break;
      }
      case POPUP_MSG_TYPE_USER_UPDATE: {
        ClientSession *_session = p_session;
        string _errMsg;
        UserUpdateMsg _msg(_message, ui->getResourceDir());
        if ((_msg.getUpdateMask() & POPUP_USER_FIELD_NICKNAME) &&
            database->updateNickname(p_session->user, _msg.getNickname(), _errMsg)) {
          p_session->user->setNickname(_msg.getNickname());
          // Reset session because we want the notification to be sent
          // to concerned user as well
          _session = 0;
        }
        if ((_msg.getUpdateMask() & POPUP_USER_FIELD_AVATAR) &&
            database->updateAvatar(p_session->user, _msg.getAvatar(), _errMsg)) {
          p_session->user->setAvatar(_msg.getAvatar());
          // Reset session because we want the notification to be sent
          // to concerned user as well
          _session = 0;
        }
        if (_msg.getUpdateMask() & POPUP_USER_FIELD_MODE) {
          p_session->user->setMode(_msg.getMode());
        }
        if (_msg.getUpdateMask() & POPUP_USER_FIELD_HEALTH) {
          p_session->user->setAlive(_msg.isAlive());
        }
        broadcastMessage(_fullMessage, _fullMessageSize, _session);
        break;
      }
      case POPUP_MSG_TYPE_RATE_MESSAGE: {
        RateUserMessageWrapper _rate(_message);
        // Forward message to other users
        sendMessage(_fullMessage, _fullMessageSize, _rate.targets);
        // Update database
        string _errMsg;
        DatabaseUI::StatisticItemUpdateResult _updateResult;
        if (database->updateUserRates(_rate.msgSenderID, _rate.rateItem,
                                      /*(_rate.isUndo?
                                          DatabaseUI::ITEM_UPDATE_DECREMENT :
                                          DatabaseUI::ITEM_UPDATE_INCREMENT),*/
                                      _updateResult, _errMsg))
        {
          // Notify everybody about this statistic update
          StatisticsUpdateMsg _statsUpdateMsg(_rate.msgSenderID);
          _statsUpdateMsg.statistics().set(_rate.rateItem,
                                           _updateResult.newItemValue,
                                           _updateResult.isNewRecord);
          broadcastMessage(&_statsUpdateMsg);

          // If record is a new one, and that recordman changed, tell
          // about the previous recordman who had been defeated
          if (_updateResult.isNewRecord &&
              (_updateResult.previousRecordman != 0) &&
              (_updateResult.previousRecordman != _rate.msgSenderID)) {
            StatisticsUpdateMsg _recordLostMsg(_updateResult.previousRecordman);
            _recordLostMsg.statistics().set(_rate.rateItem,
                                            _updateResult.previousRecordValue,
                                            false);
            broadcastMessage(&_recordLostMsg);
          }
        }
        break;
      }
      case POPUP_MSG_TYPE_CANVASS_SUBMIT: {
        CanvassSubmitMsg _msg(_message);
        sendMessage(_fullMessage, _fullMessageSize, _msg.targets);
        break;
      }
      case POPUP_MSG_TYPE_CANVASS_VOTE: {
        CanvassVoteMsg _vote(_message);
        sendMessage(_fullMessage, _fullMessageSize, _vote.targets);
        break;
      }
      case POPUP_MSG_TYPE_INVITATION: {
        ThreadInvitationMsg msg(_message);
        sendMessage(_fullMessage, _fullMessageSize, msg.cc);
        sendMessage(_fullMessage, _fullMessageSize, msg.invitedUsers);
        break;
      }
      case POPUP_MSG_TYPE_KEEPALIVE_PROBE: {
        KeepAliveAck _ack;
        if (!p_session->networker.send(&_ack)) {
          p_session->disconnect = true;
        }
        break;
      }
      case POPUP_MSG_TYPE_KEEPALIVE_ACK: {
        break;
      }
      default:
        info("Received a message with unexpected ID %d",
             _message->header.type);
        break;
    }

    if (_message) {
      delete(_message);
    }
    if (_fullMessage) {
      delete(_fullMessage);
    }
  } else {
    _closeConnection = true;
  }

  // Client disconnected
  return (!_closeConnection);
}

void ServerImpl::deleteSession(ClientSession *p_session)
{
  if (p_session->registered) {
    info("Client %s disconnected",
        p_session->user->getLogin().c_str());
    sessions.erase(p_session->user->getID());

    p_session->user->alive = false;
    UserUpdateMsg _deadUserUpdateMessage(*p_session->user,
                                         POPUP_USER_FIELD_HEALTH);
    broadcastMessage(&_deadUserUpdateMessage);
  } else {
    trace("Client disconnected");
  }
  delete p_session;
}

void ServerImpl::broadcastMessage(AbstractMessage *p_message,
                                  const ClientSession *p_from)
{
  Sessions::iterator it;
  for (it = sessions.begin(); it != sessions.end(); it++) {
    ClientSession *_to = it->second;
    if ((p_from == 0) || (_to != p_from)) {
      bool _success = _to->networker.send(p_message);
      if (!_success) {
        _to->disconnect = true;
      }
    }
  }
}

void ServerImpl::broadcastMessage(const RawMessage *p_message,
                                  unsigned p_messageSize,
                                  const ClientSession *p_from)
{
  Sessions::iterator it;
  for (it = sessions.begin(); it != sessions.end(); it++) {
    ClientSession *_to = it->second;
    if ((p_from == 0) || (_to != p_from)) {
      bool _success = _to->networker.send(p_message, p_messageSize);
      if (!_success) {
        _to->disconnect = true;
      }
    }
  }
}

void ServerImpl::sendMessage(const RawMessage *p_message,
                             unsigned p_messageSize,
                             const UserList & p_targets)
{
  UserList::const_iterator _itTarget;
  Sessions::const_iterator _itSession;

  for (_itTarget = p_targets.begin(); _itTarget != p_targets.end(); _itTarget++) {
    _itSession = sessions.find(*_itTarget);
    if (_itSession != sessions.end()) {
      bool _success = _itSession->second->networker.send(p_message, p_messageSize);
      if (!_success) {
        _itSession->second->disconnect = true;
      }
    }
  }
}

bool ServerImpl::sendAllUsersList(ClientSession *p_session)
{
  UserLoginMap::iterator _it;
  bool _rc = true;
  string _errMsg;

  for (_it = loginsMap.begin(); _rc && (_it != loginsMap.end()); _it++) {
    // Send user info
    User *_user= _it->second;
    if (_user->getID() != p_session->user->getID()) {
      UserUpdateMsg _newUserUpdateMessage(*_user);
      _rc = p_session->networker.send(&_newUserUpdateMessage);
    }
    if (_rc) {
      // Send user statistics
      StatisticsUpdateMsg _statsUpdateMessage(_user->getID());
      if (database->getUserStatitics(_user->getID(),
          _statsUpdateMessage.statistics(), _errMsg)) {
        _rc = p_session->networker.send(&_statsUpdateMessage);
      }
    }
    if (!_rc) {
      p_session->disconnect = true;
    }
  }
  return _rc;
}

void ServerImpl::closeAllConnections()
{
  trace("Closing all connections");
  Sessions::iterator _it;
  for (_it = sessions.begin(); _it != sessions.end(); _it++) {
    delete _it->second;
  }
  sessions.clear();
  sockets.clear();
  if (serverSocket != 0) {
    serverSocket->close();
  }
}
